跳到主要内容

Go 的 HTTP 标准库-客户端-工作原理

客户端可以直接通过 net/http.Get 使用默认的客户端 net/http.DefaultClient 发起 HTTP 请求

resp, err := http.Get("https://baidu.com")

这个 net/http.Get 里面就是

func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}

也可以自己构建新的 net/http.Client 实现自定义的 HTTP 请求

func main() {
client := &http.Client{}
req, _ := http.NewRequest("GET", "https://baidu.com", nil)
resp, _ := client.Do(req)
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf(string(body))
}

在多数情况下使用默认的客户端都能满足我们的需求,不过需要注意的是使用默认客户端发出的请求没有超时时间,所以在某些场景下会一直等待下去。

事务和 Cookie 是 HTTP 客户端包为我们提供的两个最重要模块

当我们调用 http.Client.Get 发出 HTTP 时,会按照如下的步骤执行:

  1. 调用 http.NewRequest 根据方法名、URL 和请求体构建请求;
  2. 调用 http.Transport.RoundTrip 开启 HTTP 事务、获取连接并发送请求;
  3. 在 HTTP 持久连接的 http.persistConn.readLoop 方法中等待响应;

整体结构如下

值得注意的是,这个 http.Transporthttp.RoundTripper 接口的实现,它的主要作用就是支持 HTTP/HTTPS 请求和 HTTP 代理;

http.persistConn 封装了一个 TCP 的持久连接,是我们与远程交换消息的句柄(Handle);

开启事务

使用标准库构建了 HTTP 请求之后,会开启 HTTP 事务发送 HTTP 请求并等待远程的响应,经过下面一连串的调用,我们最终来到了标准库实现底层 HTTP 协议的结构体 http.Transport

  • http.Client.Do
  • http.Client.do
  • http.Client.send
  • http.send
  • http.Transport.RoundTrip

这个 http.Transporthttp.RoundTripper 接口的实现

type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}

http.Transport 会在 http.Transport.roundTrip 中发送 HTTP 请求并等待响应(如下可见 RoundTrip 调用的这个 roundTrip)

func (t *Transport) RoundTrip(req *Request) (*Response, error) {
return t.roundTrip(req)
}

可以将该函数的执行过程分成两个部分:

根据协议去取对应的 RoundTripper

根据 URL 的协议查找并执行自定义的 net/http.RoundTripper 实现;

func (t *Transport) roundTrip(req *Request) (*Response, error) {
ctx := req.Context()
scheme := req.URL.Scheme

if altRT := t.alternateRoundTripper(req); altRT != nil {
if resp, err := altRT.RoundTrip(req); err != ErrSkipAltProtocol {
return resp, err
}
}
...
}

可以在标准库的 net/http.Transport 中调用 net/http.Transport.RegisterProtocol 为不同的协议注册 net/http.RoundTripper 的实现(其实它内部就是一个 map[string]RoundTripper

提示

Transport 作为一个可以复用的结构体实际上可以处理不同协议的请求,那么不同协议的请求就要有不同的实现,诸如 ftp,file 等。如果出现了这种情况,我们就可以通过 RegisterProtocol 来注册一些针对不同协议的实现,从而当 Transport 发送 Request 之前就可以通过 map 来确定到底要使用哪个 RoundTrip。

例如 http2 包就是这样注册进来的

func http2registerHTTPSProtocol(t *Transport, rt http2noDialH2RoundTripper) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("%v", e)
}
}()
t.RegisterProtocol("https", rt)
return nil
}

取得连接

从连接池中获取或者初始化新的持久连接并调用连接的 net/http.persistConn.roundTrip 发出请求;

func (t *Transport) roundTrip(req *Request) (*Response, error) {
...
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}

treq := &transportRequest{Request: req, trace: trace}
cm, err := t.connectMethodForRequest(treq)
if err != nil {
return nil, err
}

pconn, err := t.getConn(treq, cm)
if err != nil {
return nil, err
}

resp, err := pconn.roundTrip(treq)
if err == nil {
return resp, nil
}
}
}

http.Transport.getConn 是获取连接的方法,该方法会通过两种方法获取用于发送请求的连接:

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
req := treq.Request
ctx := req.Context()

w := &wantConn{
cm: cm,
key: cm.key(),
ctx: ctx,
ready: make(chan struct{}, 1),
}

if delivered := t.queueForIdleConn(w); delivered {
return w.pc, nil
}

t.queueForDial(w)
select {
case <-w.ready:
...
return w.pc, w.err
...
}
}
  • 调用 http.Transport.queueForIdleConn 在队列中等待闲置的连接;
  • 调用 http.Transport.queueForDial 在队列中等待建立新的连接;

连接是一种相对比较昂贵的资源,如果在每次发出 HTTP 请求之前都建立新的连接,可能会消耗比较多的时间,带来较大的额外开销,通过连接池对资源进行分配和复用可以有效地提高 HTTP 请求的整体性能,多数的网络库客户端都会采取类似的策略来复用资源。

而当上面没有空闲的连接时,就需要创建一个连接

调用 http.Transport.queueForDial 尝试与远程建立连接时,标准库会在内部启动新的 Goroutine 执行 http.Transport.dialConnFor 用于建连,最终调用的 http.Transport.dialConn

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
pconn = &persistConn{
t: t,
cacheKey: cm.key(),
reqch: make(chan requestAndChan, 1),
writech: make(chan writeRequest, 1),
closech: make(chan struct{}),
writeErrCh: make(chan error, 1),
writeLoopDone: make(chan struct{}),
}

conn, err := t.dial(ctx, "tcp", cm.addr())
if err != nil {
return nil, err
}
pconn.conn = conn

pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())
pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())

go pconn.readLoop()
go pconn.writeLoop()
return pconn, nil
}

References